#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <ctype.h>
#include <stdarg.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>

#include <pthread.h>

#include <errno.h>
#include <netdb.h>

#include <dictionary.h>
#include <iniparser.h>

#include <kxml.h>
#include <linked_list.h>

#include "remote_log.h"

#undef DEBUG

#ifdef DEBUG
#define DBG(x...) do { \
	fprintf (stderr, x); \
} while (0)
#else
#define DBG(x...)
#endif

/* default remote log server port */
#define RLOG_PORT_DEFAULT 50005

/* each log line is separated by RLOG_SEPARATOR */
#define RLOG_SEPARATOR	"-----\r\n\r\n\0"
static char rlog_separator[] = RLOG_SEPARATOR;

/*
 * This file sends a log message generated by log.c to remote logging server.
 */
#ifdef HAVE_REMOTE_LOG
static struct sockaddr_in *remote_server = NULL;
static char *remote_server_name = NULL;
static int remote_sd = -1; 

static char *hostname = NULL;

static list rlog_queue;
static pthread_mutex_t rlog_queue_lock;
struct queued_rlog
{
	char *hdr;
	char *string;
	time_t t;
};

static pthread_t connect_thread;

static int _rlog_init = 0; 
#define IS_INIT		(_rlog_init == 1)

static int rlog_connect (void);
static int rlog_disconnect (void);
static void* connect_thread_func (void *arg);

/**
 * Initializes the remote logging feature.
 * 
 * @param	conf pointer to the ini-parser data.
 * @return  none
 */
void rlog_init (dictionary *conf)
{
	/* for now, only one server */
	char *server;
	char *svrname;
	char *port;
	struct hostent *hp;

	DBG ("rlog_init: start\n");

	if ((svrname = iniparser_getstring(conf, "KENS:server_name", "KENS")) == NULL)
	{
		svrname = "KENS";
	}

	hostname = strdup (svrname);

	if ((server = iniparser_getstring(conf, "KENS:remote_log", NULL)) == NULL)
	{
		// for debug
		server = "127.0.0.1\0";
	}

	rlog_set_server (server);

	//rlog_connect ();

	pthread_mutex_init (&rlog_queue_lock, NULL);
	rlog_queue = list_open();

	pthread_create (&connect_thread, NULL, &connect_thread_func, NULL);

	_rlog_init = 1;
}

/**
 * Stops the remote logging feature.
 * 
 * @param	conf pointer to the ini-parser data.
 * @return  none
 */
void rlog_shutdown (void)
{
	if (!IS_INIT)
		return;

	pthread_cancel (connect_thread);
	rlog_disconnect ();
}

/**
 * Periodically checks for the connection. If the connection is dead, 
 * try to connect.
 * 
 * @param arg not used
 * @return  none
 */
static void* connect_thread_func (void *arg)
{
	while (1)
	{
		if (remote_server != NULL &&
				remote_sd == -1)
		{
			rlog_connect ();
		}

		sleep (1); /* every 1s */
	}

	pthread_exit (NULL);
}

/**
 * send the log with the designated time. XML for a log is constructed
 * in the function using libxml2. In case of the dead connection, a log
 * is queued and will be sent later.
 * 
 * @param	hdr header for the log
 *			string line of the log
 *			t time of creation
 * @return  none
 */
static void rlog_send_time (const char *hdr, const char *string, time_t t)
{
	int sock;

	int rc;
	xmlTextWriterPtr writer;
	xmlBufferPtr buf;
	xmlChar *tmp;

	struct tm *ts = NULL;

	char small_buf[32];

	if (!IS_INIT)
		return;

	//sock = rlog_connect ();
	sock = remote_sd;

	if (sock < 0)
	{
		/* failed to connect to the remote server */
		/* queue the message */
		struct queued_rlog *log = (struct queued_rlog*)malloc(sizeof(struct queued_rlog));
		memset (log, 0, sizeof(struct queued_rlog));
		if (hdr != NULL)
			log->hdr = strdup (hdr);
		if (string != NULL)
			log->string = strdup (string);
		log->t = t;

		pthread_mutex_lock (&rlog_queue_lock);
		list_add_head (rlog_queue, (void*)log);
		pthread_mutex_unlock (&rlog_queue_lock);
		return;
	}

	/* Create a new XML buffer, to which the XML document will be
	 * written */
	buf = xmlBufferCreate();
	if (buf == NULL) {
		DBG("testXmlwriterMemory: Error creating the xml buffer\n");
		return;
	}

	/* Create a new XmlWriter for memory, with no compression.
	 * Remark: there is no compression for this kind of xmlTextWriter */
	writer = xmlNewTextWriterMemory(buf, 0);
	if (writer == NULL) {
		DBG("testXmlwriterMemory: Error creating the xml writer\n");
		return;
	}

	/* Start the document with the xml default for the version,
	 * encoding ISO 8859-1 and the default for the standalone
	 * declaration. */
	rc = xmlTextWriterStartDocument(writer, NULL, KXML_ENCODING, NULL);
	if (rc < 0) {
		DBG
			("testXmlwriterMemory: Error at xmlTextWriterStartDocument\n");
		return;
	}

	/* Start an element named "log". Since thist is the first
	 * element, this will be the root element of the document. */
	rc = xmlTextWriterStartElement(writer, BAD_CAST "log");
	if (rc < 0) {
		DBG
			("testXmlwriterMemory: Error at xmlTextWriterStartElement\n");
		return;
	}

	tmp = ConvertInput(hostname, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "hostname", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	
	/* Start an element named "date" as child of ORDER. */
	rc = xmlTextWriterStartElement(writer, BAD_CAST "date");
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterStartElement\n");
		return;
	}

	ts = localtime(&t);

	memset (small_buf, 0, 32);

	sprintf (small_buf, "%d", ts->tm_year + 1900);
	tmp = ConvertInput(small_buf, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "year", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	sprintf (small_buf, "%d", ts->tm_mon + 1);
	tmp = ConvertInput(small_buf, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "month", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	sprintf (small_buf, "%d", ts->tm_mday);
	tmp = ConvertInput(small_buf, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "day", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	sprintf (small_buf, "%d", ts->tm_hour);
	tmp = ConvertInput(small_buf, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "hour", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	sprintf (small_buf, "%d", ts->tm_min);
	tmp = ConvertInput(small_buf, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "minute", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	sprintf (small_buf, "%d", ts->tm_sec);
	tmp = ConvertInput(small_buf, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "second", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	/* Close the element named "date". */
	rc = xmlTextWriterEndElement(writer);
	if (rc < 0) {
		DBG("testXmlwriterMemory: Error at xmlTextWriterEndElement\n");
		return;
	}

	sprintf (small_buf, "%d", getpid());
	tmp = ConvertInput(small_buf, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "pid", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	if (hdr != NULL)
	{
		sprintf (small_buf, "%s", hdr);
		tmp = ConvertInput(small_buf, KXML_ENCODING);
		rc = xmlTextWriterWriteElement(writer, BAD_CAST "header", tmp);
		if (rc < 0) {
			DBG
				("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
			return;
		}
		if (tmp != NULL) ConvertFree(tmp);
	}

	tmp = ConvertInput(string, KXML_ENCODING);
	rc = xmlTextWriterWriteElement(writer, BAD_CAST "line", tmp);
	if (rc < 0) {
		DBG
			("testXmlwriterFilename: Error at xmlTextWriterWriteElement\n");
		return;
	}
	if (tmp != NULL) ConvertFree(tmp);

	/* Close the element named "log". */
	rc = xmlTextWriterEndElement(writer);
	if (rc < 0) {
		DBG("testXmlwriterMemory: Error at xmlTextWriterEndElement\n");
		return;
	}

	xmlFreeTextWriter(writer);

	//DBG ("%s", (const char*)buf->content);

	if (send (sock, (const void*)buf->content, buf->use, 0) == -1)
		return;

	if (send (sock, (const void*)rlog_separator, strlen(rlog_separator), 0) == -1)
		return;

	xmlBufferFree (buf);

	return;
}

/**
 * send the log with the current time.
 * 
 * @param	hdr header for the log
 *			string line of the log
 * @return  none
 */
void rlog_send (const char *hdr, const char *string)
{
	if (!IS_INIT)
		return;

	rlog_send_time (hdr, string, time(NULL));
}


/**
 * not implemented.
 * 
 * @param	hdr header for the log
 *			format format string for the line
 * @return  none
 */
void rlog_send_format (const char *hdr, const char *format, ...)
{
	/* TODO */
}

/**
 * set the address of the remote logging server.
 * 
 * @param	address address of the remote logging server either raw IP or domain.
 * @return  0 for success
 */
int rlog_set_server (const char *address /* domain name or ip */)
{
	struct sockaddr_in *new_server = NULL;

	char hostname[128];
	size_t hostname_len = 0;
	char *port;
	struct hostent *hp;

	memset (hostname, 0, 128);

	port = strstr(address,":");
	if (port == NULL)
		hostname_len = strlen(address);
	else
		hostname_len = (size_t)(strstr (address, ":") - address);
	memcpy (hostname, address, hostname_len);

	hp = gethostbyname(hostname);

	if (hp == NULL)
	{
		DBG ("Unable to resolve for %s\n", hostname);
		return -1;
	}

	/* for the first ip address */
	if (hp->h_addr_list[0] != NULL)
	{
		new_server = (struct sockaddr_in*)malloc (sizeof(struct sockaddr_in));
		if (new_server != NULL)
		{
			memcpy ((void*)&new_server->sin_addr, (void*)hp->h_addr_list[0], sizeof(struct in_addr));
			if (port != NULL)
				new_server->sin_port = htons(atoi(port + 1));
			else
				new_server->sin_port = htons(RLOG_PORT_DEFAULT);
			new_server->sin_family = AF_INET;
		}
	}

	if (remote_server != NULL)
	{
		free (remote_server);
	}

	if (remote_server_name != NULL)
	{
		free (remote_server_name);
	}

	remote_server = new_server;
	remote_server_name = strdup (address);

	rlog_disconnect ();

	return 0;
}

/**
 * return the remote server's address.
 * 
 * @param	none
 * @return  string
 */
const char* get_remote_server_name (void)
{
	return remote_server_name;
}

/**
 * try to establish the connection to the remote logging server.
 * when successful, flush logs from the queue.
 * 
 * @param	none
 * @return  socket descriptor
 */
static int rlog_connect (void)
{
	int sock;
	int err = -1;

	if (remote_sd != -1)
		return remote_sd;

	if (remote_server == NULL)
		return -1;

	sock = socket (AF_INET, SOCK_STREAM, 0);

	if (sock == -1)
		return -errno;

	DBG ("Connecting to Remote Log Server %s:%d\n",
			inet_ntoa(remote_server->sin_addr.s_addr), 
			ntohs(remote_server->sin_port));

	err = connect (sock, (struct sockaddr*)remote_server, sizeof(struct sockaddr_in));
	if (err == -1)
	{
		DBG ("\tFailed: %d - %s\n", errno, strerror(errno));
		close (sock);
		sock = -1;
		return -errno;
	}

	remote_sd = sock;

	{
		/* flush the queue */
		struct queued_rlog *rlog;

		pthread_mutex_lock (&rlog_queue_lock);

		while ((rlog = list_remove_head(rlog_queue)) != NULL)
		{
			if (rlog != NULL)
			{
				rlog_send_time (rlog->hdr, rlog->string, rlog->t);
				if (rlog->hdr != NULL)
					free(rlog->hdr);
				if (rlog->string != NULL)
					free(rlog->string);
				free (rlog);
			}
		}

		pthread_mutex_unlock (&rlog_queue_lock);
	}

	return sock;
}

/**
 * tear down the connection with the remote logging server.
 * 
 * @param	none
 * @return  0 for success
 */
static int rlog_disconnect (void)
{
	if (remote_sd != -1)
	{
		close (remote_sd);
	}

	return 0;
}

#else /* !HAVE_REMOTE_LOG */
void rlog_init (dictionary *conf)
{
}

void rlog_shutdown (void)
{
}

void rlog_send (const char *hdr, const char *string)
{
}

void rlog_send_format (const char *hdr, const char *format, ...)
{
}
#endif /* HAVE_REMOTE_LOG */
